#!/usr/bin/env jruby

require 'color'

# ProcessingFinder gets included in things that need to search the tree
module ProcessingFinder
  # Search through the objects inside this one, matching using find_all 
  # and a block or optionally matching on class name or a hash of parameters
  #
  def search x=nil, &block
    if self.is_a? Sketch
      o = self.container
    else
      o = self
    end
    results = []
    if block
      results += o.find_all &block
    else
      o.each do |e|
        if x.is_a? Class
          results << e if x == e.class
        elsif x.is_a? Hash
          # TODO: Should this be an OR like this, or should it require a 
          # match on everything in the hash?
          results << e if x.any? do |k,v|
            if e.respond_to? k
              begin
                (e.send(k) == v)
              rescue
                false
              end
            end
          end
        else
          results << e if x.to_s == e.to_s
        end
      end
    end
    o.each { |e| results += e.search(x, &block) if e.respond_to? :search }
    results
  end
end

# Sketch inherits from PApplet and is the main event, holding a 
# container object that contains the rest of the objects.
#
# It also maintains a hash for caching values or whatever, and attempts 
# to handle key presses.
#
class Sketch < PApplet
  include ProcessingFinder

  attr_accessor :dump, :frame_count

  def initialize &block
    super

    self[:width] ||= 400
    self[:height] ||= 300
    self[:frame_rate] ||= 30

    begin
      container
    rescue
      puts "Sketch containment field is failing, Gordon: #{$!}"
    end
  end

  def [] k
    @hash ||= Hash.new
    @hash[k]
  end

  def []= k, v
    @hash ||= Hash.new
    @hash[k] = v
  end

  # This is so Jankity can know what sketch object was most recently loaded
  def Sketch.inherited c
    super
    @@last_inherited = c
  end

  # Set by Sketch.inherited
  def Sketch.last_inherited
    if defined? @@last_inherited
      @@last_inherited
    else
      Sketch
    end
  end

  # This gets called by Jankity when the sketch needs to actually 
  # display.
  #
  # The JFrame may behave differently on different OSes and JVM 
  # versions, you may want to try sleeping between the frame.visible and 
  # self.init if there are issues with display.
  def display
    @frame = javax.swing.JFrame.new(self.class.to_s)
    @frame.content_pane.add self
    @frame.default_close_operation = javax.swing.JFrame::EXIT_ON_CLOSE
    @frame.set_size self[:width], self[:height]
    @frame.visible = true
    self.init
  end

  # Saves the current frame to an output directory. Debugging/movie-making use.
  def dump
    filename =
    if ENV['name']
      File.basename(ENV['name'], ".rb")
    else
      self.class.to_s
    end
    saveFrame("out/#{filename}_"+sprintf("%06d", frameCount-1)+".png");
  end

  # Initializes the size of the sketch and such
  def setup
    size self[:width], self[:height], self[:type]
    color_mode (self[:color_type] or HSB), 1
    frame_rate self[:frame_rate]

    self[:dump] = true if ENV['frames']

    @frame_count = 0
  end

  # Calls draw on the main container object. If there are problems, it 
  # prints a backtrace, waits 10 seconds, and attempts to reload. 
  def draw
    begin
      @container.draw
    rescue => e
      puts "Error in drawing, not caught by container: #{e.to_s}"
      puts e.backtrace
      sleep 10
      reload
    end

    dump if self[:dump]

    @frame_count += 1

    exit if ENV['frames'].to_i > 0 and frameCount.to_i > ENV['frames'].to_i
  end

  # Override the builtin frameCount to allow resetting it
  def frameCount
    @frame_count
  end

  def frameCount= x
    @frame_count = x.to_i
  end

  # TODO: Key functions need supreme work
  def keyPressed k
    puts k.inspect
    puts k.keyText()
    @keys[k.keyText.to_s] = true
  end

  def keyReleased *args, &block
    @keys = {}
  end

  # Hash containing any keys currently pressed
  def keys 
    @keys ||= {}
    @keys
  end
  
  #--
  # TODO: Does not work... need to figure out how to reset the frame 
  # count of the sketch so that realtime reloading can start "fresh"
  def reset_frame
    frameCount = 0
  end

  def current_frame
    frameCount
  end

  # Attempts to cleanly kill the sketch object.
  #--
  # TODO: Need to call this from irb#exit someday.
  def clean_exit
    self.stop
    self.exit
  end
end

class Sketch3D < Sketch
  def initialize
    self[:type] = OPENGL

    if not friendly_class "processing.opengl.PGraphicsOpenGL" 
      puts "OpenGL failure, falling back to P3D"
      self[:type] = P3D
    end

    super
  end

  # Container when called with a block initializes a new object to hold 
  # things with that block. Otherwise, it returns the current container.
  def container &block
    if block
      @container = Container.new self, &block
    end
    @container.block do
      translate(@sketch[:width]/2, @sketch[:height]/2, 0.0)
      scale(@sketch[:width]/4)
    end
    @container
  end
end

class Sketch2D < Sketch
  def initialize
    self[:type] = P2D
    super
  end

  # Container when called with a block initializes a new object to hold 
  # things with that block. Otherwise, it returns the current container.
  def container &block
    if block
      @container = Container.new self, &block
    end
    @container
  end
end


# Math shortcuts for pulsing based on frameCount and other things
module Shortcuts
  # Bounces from 0 to 1 and back to 0 with speed x in a sine wave 
  def sin_bounce x
    Math::sin(Math::PI * (frameCount.to_f % x.to_f) / x.to_f)
  end

  # Bounces from 0 to 1 and back to 0 with speed x in a cosine wave 
  def cos_bounce x
    Math::cos(Math::PI * (frameCount.to_f % x.to_f) / x.to_f)
  end

  # Pulses from -1 to 1 and back with speed x in a sine wave
  def sin_pulse x
    Math::sin(Math::PI * 2 * (frameCount.to_f % x.to_f) / x.to_f)
  end

  # Pulses from -1 to 1 and back with speed x in a cosine wave
  def cos_pulse x
    Math::cos(Math::PI * 2 * (frameCount.to_f % x.to_f) / x.to_f)
  end

  alias smooth_pulse sin_pulse

  # Rotate through 2*PI based on frameCount every x frames
  def pi_pulse x
    Math::PI * 2 * (frameCount.to_f % x.to_f) / x.to_f
  end

  # Pulse from 0 to 1 every x frames
  def pulse x
    (frameCount.to_f % x.to_f) / x.to_f
  end

  def norm x, n
    (n.to_f % x.to_f) / x.to_f
  end

  def r_s
    random(-1,1)
  end

  def r_PI
    random(-1,1) * Math::PI
  end

  # Random degree rotation: 0-90
  def r_90
    Math::PI / 2.0 * r_s
  end

  # Random degree rotation: 0-180
  def r_90
    Math::PI * r_s
  end

  def r_u
    random(0,1)
  end
end


module ProcessingParser
  def parent= x
    @parent = x
  end

  def parent
    @parent or @sketch
  end

  def add_proc opts={}, &block
    @procs ||= []
    @procs << [opts, block]
  end

  alias block add_proc

  def between a, b, opts={}, &block
    opts[:before] = a
    opts[:after] = b
    add_proc opts, &block
  end

  def cycle n=nil, opts={}, &block
    if n
      opts[:cycle] = n
      add_proc opts, &block
    else
      @cycle
    end
  end

  def after &block
    @after = block
  end

  def group opts = {}, &block
    c = opts[:class] || Group
    self << c.new(@sketch, opts, &block)
  end

  alias add group
  alias object group

  def line *args, &block
    puts "Adding a new line!" if $DEBUG
    self << Line.new(@sketch, *args, &block)
  end

  def box *args, &block
    puts "Adding a new box!" if $DEBUG
    self << Box.new(@sketch, *args, &block)
  end

  def sphere *args, &block
    puts "Adding a new sphere!" if $DEBUG
    self << Sphere.new(@sketch, *args, &block)
  end

  def axes 
    line :x => 1, :stroke => Red do
      line :x => 1, :xe => 0.9, :ye => 0.1
    end
    line :y => 1, :stroke => Blue do
      line :y => 1, :ye => 0.9, :xe => -0.1
    end
    line :z => 1, :stroke => Green do
      line :z => 1, :ze => 0.9, :ye => -0.1
    end
  end

  def method_missing id, *args
    if @parent.respond_to? id
      @parent.send id, *args
    elsif @sketch.respond_to? id
      @sketch.send id, *args
    else
      raise "No method #{id} for #{self.inspect}!"
    end
  end
end


module ProcessingTransform
  attr_accessor :tx, :ty, :tz
  attr_accessor :rx, :ry, :rz
  attr_accessor :sx, :sy, :sz
  attr_accessor :x, :y, :z

  def transform_init opts = {}
    @tx = opts[:tx] || 0
    @ty = opts[:ty] || 0
    @tz = opts[:tz] || 0

    @rx = opts[:rx] || 0
    @ry = opts[:ry] || 0
    @rz = opts[:rz] || 0

    @sx = opts[:sx] || opts[:s] || 1
    @sy = opts[:sy] || opts[:s] || 1
    @sz = opts[:sz] || opts[:s] || 1

    @x = opts[:x] || 1
    @y = opts[:y] || 1
    @z = opts[:z] || 1
  end

  def transform_start
    return if @disable_transform
    return if @transformed == true
    @transformed = true
    @sketch.push_matrix
    @sketch.translate @tx, @ty, @tz if @tx != 0 or @ty != 0 or @tz != 0
    @sketch.scale     @sx, @sy, @sz if @sx != 1 or @sy != 1 or @sz != 1
    @sketch.rotateX @rx if @rx != 0
    @sketch.rotateY @ry if @ry != 0
    @sketch.rotateZ @rz if @rz != 0
  end

  def transform_stop
    return if @disable_transform
    @sketch.pop_matrix if @transformed == true
    @transformed = false
  end
end


class Container < Array
  include ProcessingFinder
  include ProcessingParser
  include ProcessingTransform
  include Shortcuts

  attr_accessor :sketch
  attr_accessor :visible

  def initialize sketch, opts = {}, &block
    @sketch = sketch
    @visible = true

    self.instance_eval &block if block

    transform_init opts
    @stroke = Color.new(opts[:stroke]) if opts[:stroke]
    @fill = Color.new(opts[:fill]) if opts[:fill]
  end

  def fill parent_depth=0
    if @fill
      @fill
    elsif @parent.respond_to? :fill and parent_depth < 10
      @parent.fill parent_depth + 1
    else
      Color.new :value => 1
    end
  end

  def stroke parent_depth=0
    if @stroke
      @stroke
    elsif @parent.respond_to? :stroke and parent_depth < 10
      @parent.stroke parent_depth + 1
    else
      nil
    end
  end

  def fill= opts
    @fill = Color.new(opts)
  end

  def stroke= opts
    @stroke = Color.new(opts)
  end

  def stop
    @sketch.stop
    exit
  end

  def error_handler string, ex
    @last_error_time ||= 0
    error_string = "#{string} error in #{self.inspect}: #{ex.to_s}"
    diff = Time.now.to_i - @last_error_time
    if diff > 10
      puts error_string
      @last_error_time = Time.now.to_i
      #puts ex.backtrace.reject { |x| x =~ /jruby/ } [0..9]
      puts ex.backtrace[0..9]
    end
    @sketch.fill 0.0, 1, 1
    @sketch.stroke 0.1, 1, 1
    @sketch.box(1, 1, 1)
  end

  def delete o=nil
    if o
      super o
    else
      parent.delete self
    end
  end

  def reparent x
    x.parent.delete x
    self << x
  end

  def draw &block
    begin
      draw_before
    rescue Exception => e
      transform_start
      error_handler "Before draw", e
    end

    if visible
      begin
        # Yield to actual sub-object (sphere, box, etc.) to draw itself, 
        # or do nothing if this is just a grouping object
        yield self if block
        # Now check if there are any sub-objects and iterate over them
        each do |x|
          x.draw
        end
      rescue Exception => e
        error_handler "Draw", e
      end
    end

    begin
      draw_after
    rescue Exception => e
      error_handler "After draw", e
      transform_stop
    end
  end

  def draw_before
    @procs ||= []
    @procs.each do |pair|
      opts, block = *pair
      if opts[:cycle] then
        @cycle = frameCount % opts[:cycle]
        if opts[:bounce] then
          if (frameCount / opts[:cycle] % 2) == 1
            @cycle = opts[:cycle] - @cycle
          end
        end
      end
      if opts[:when] then
        if opts[:when].is_a? Integer
          next if frame_count != opts[:when]
        elsif opts[:when].is_a? Range
          next unless opts[:when] === frame_count
        elsif opts[:when].is_a? Proc
          next unless opts[:when].call self
        end
      end
      self.instance_eval &block
    end

    transform_start
    if stroke
      @sketch.stroke(stroke.hue, stroke.saturation, stroke.value, stroke.alpha)
    else
      @sketch.noStroke
    end

    if fill
      @sketch.fill(fill.hue, fill.saturation, fill.value, fill.alpha)
    else
      @sketch.noFill
    end
  end

  def draw_after
    self.instance_eval &@after if @after

    transform_stop
  end

  def inspect
    contents = collect { |x| x.inspect }.join(", ")
    self.class.to_s +
    if contents != ""
      " [" + contents + "]"
    else
      ""
    end
  end

  def to_s
    variables = self.instance_variables.sort.collect do |x|
      "#{x}=#{self.instance_variable_get(x)}"
    end.join ", "
    self.class.to_s + " " + variables
  end

  def << x
    x.parent = self
    super x
  end
end

class Group < Container
end

class Line < Container
  def initialize sketch, opts = {}, &block
    super sketch, opts, &block
    @xe = opts[:xe] || 0
    @ye = opts[:ye] || 0
    @ze = opts[:ze] || 0

    @x = opts[:x] || 0
    @y = opts[:y] || 0
    @z = opts[:z] || 0
    return self
  end

  def draw
    super do
      if @xe
        @sketch.line(@x, @y, @z, @xe, @ye, @ze)
      else
        @sketch.line(0, 0, 0, @x, @y, @z)
      end
    end
  end
end

class Box < Container
  def draw
    super do
      @sketch.box(@x, @y, @z)
    end
  end
end

class Sphere < Container
  def draw
    super do
      @sketch.sphere x
    end
  end
end

class Image < Container
  def initialize sketch, opts = {}, &block
    super sketch, opts, &block
    @image = opts[:image]
    return self
  end

  def draw
    # TODO
  end
end


class Particles < Container
  def initialize sketch, opts = {}, &block
    friendly_class "traer.physics.ParticleSystem"
    friendly_class "traer.physics.Particle"

    # TODO: Make this based on options
    @system = ParticleSystem.new(0, 0.2)
    @system.clear
    @particles = []

    if opts[:max_age]
      @max_age = opts[:max_age]
    end

    case opts[:gravity]
    when :attraction:
      @attraction = true
    when :springs:
      @springs = true
    else
      if opts[:gravity].is_a? Array
        @system.set_gravity *opts[:gravity]
      else
        @gravity = opts[:gravity].to_f
        @gravity ||= 0.05
        @system.set_gravity @gravity
      end
    end

    super sketch, opts, &block
  end

  def system
    @system
  end

  def particle opts = {}, &block
    opts[:x] ||= 0
    opts[:y] ||= 0
    opts[:z] ||= 0
    opts[:vx] ||= 0
    opts[:vy] ||= 0
    opts[:vz] ||= 0
    opts[:mass] ||= 1
    p = @system.make_particle opts[:mass], opts[:x], opts[:y], opts[:z]
    p.setVelocity opts[:vx], opts[:vy], opts[:vz]
    if @attraction
      each do |x|
        @system.makeAttraction p, x.particle, 0.05, 0.02
      end
    end

    po = ParticleObject.new @sketch, :particle => p, &block
    self << po
    return po
  end

  def draw
    @system.tick
    # Check for any particles past max_age
    if @max_age
      self.reject! { |i| i.particle.age > @max_age }
      # TODO: Remove those particles from the system as well
      #@system
    end
    super do
    end
  end
end

class ParticleObject < Container
  attr_accessor :particle

  def initialize sketch, opts = {}, &block
    super sketch, opts, &block
    @particle = opts[:particle]
  end

  def transform_start
    super
    @sketch.translate @particle.position.x, @particle.position.y, @particle.position.z if @particle
  end
end


module PHelper
  class Camera3D < Container
    # TODO: Use actual camera calls
    def draw 
      super do
        translate(@sketch[:width]/2, @sketch[:height]/2, 0.0)
        scale(@sketch[:width]/4)
        #camera(
        #  0, 0, 8,
        #  0, 0, 0,
        #  0, 1, 0)
      end
    end
  end

  class CameraMouse < Container
    def draw
      super do
        @mouse_x ||= 0
        @mouse_y ||= 0
        @mouse_x_new ||= 0
        @mouse_y_new ||= 0

        @mouse_x_new = mouseX/@sketch[:width].to_f * 2 * Math::PI
        @mouse_y_new = mouseY/@sketch[:height].to_f * 2 * Math::PI

        diff = @mouse_x - @mouse_x_new
        if (diff.abs >  0.01)
          @mouse_x -= diff/4.0
        end

        diff = @mouse_y - @mouse_y_new
        if (diff.abs >  0.01) 
          @mouse_y -= diff/4.0
        end

        rotateX -1 * @mouse_y
        rotateY -1 * @mouse_x
      end
    end
  end

  class Sound < Container
    attr_reader :bpm, :beat, :tap, :fade_time

    def initialize sketch, opts = {}, &block
      super sketch, opts, &block

      @bpm = opts[:bpm] || 120
      @sketch[:bpm] = @bpm
      @beat = 0
      @tap = 0
      @fade_time = opts[:fade_time] || 20
    end

    def beat
      @beat
    end

    def draw_before
      fpm = @sketch[:frame_rate] * 60
      fpb = fpm / bpm.to_f
      if frameCount % fpb <= 1.0
        @beat = 1
      else
        @beat -= 1 / @fade_time.to_f
        @beat = 0 if @beat <= 0
      end
      @sketch[:beat] = @beat
      super
    end
  end

  class SoundMinim < Sound
    attr_reader :bars

    def initialize sketch, opts = {}, &block
      super sketch, opts, &block

      begin
        file = opts[:file] or raise "No file specified for Sound object"
        @bars = opts[:bars] || 128

        friendly_class "ddf.minim.Minim"
        friendly_class "ddf.minim.AudioPlayer"
        friendly_class "ddf.minim.analysis.FFT"

        Minim.start(@sketch)

        @sound = Minim.loadFile(file)
        @fft = FFT.new(@sound.bufferSize, @sound.sampleRate);

        if opts[:linear]
          @fft.linAverages(opts[:linear])
        elsif opts[:log]
          @fft.logAverages(22, opts[:log])
        end

        if opts[:realtime]
          @sound.play
          block do
            if (frameCount * self[:frame_rate] / 60 % @bpm) == 0
              @beat = 1 
            else
              @beat -= @fade_time
            end

            @fft.forward(@sound.mix)
          end
        else
          block do
            # TODO: Awwww c'mon -- need something that isn't realtime 
            # dependent so I can render high quality frames on crappy hardware
            #c = (frameCount / (@sketch.[:frame_rate]).to_f * 100.0).to_i
            #puts "Trying to cue to #{c}"
            #@sound.rewind
            #@sound.skip c
          end
        end

      rescue
        puts "Sound failure: #{$!}"
      end
      return self
    end

    def fft x
      @fft.getBand(x)
    end

    def fft_each
      (0..@bars-1).each do |i|
        yield @fft.getBand(x)
      end
    end

    def fft_each_with_index
      (0..@bars-1).each do |i|
        yield @fft.getBand(x), i
      end
    end
  end

  class SoundEss < Sound
    attr_accessor :owner
    attr_reader :bars

    def initialize sketch, opts = {}, &block
      super sketch, opts, &block

      friendly_class "krister.Ess.Ess"
      friendly_class "krister.Ess.FFT"
      friendly_class "krister.Ess.AudioChannel"

      file = opts[:file] or raise "No file specified for Sound object"
      @sketch = sketch
      @bars = opts[:bars] || 123
      Ess.start(owner);
      @channel = AudioChannel.new(file)
      @fft = FFT.new(bars * 2)
      @fft.limits
    end

    def draw
      #sample_rate = @channel.sampleRate 
      sample_rate = 44100
      x = (owner.current_frame * sample_rate / owner.v_frame_rate).to_i
      @fft.getSpectrum(@channel.samples, x);
    end

    def fft x
      @fft.spectrum[x]
    end

    def fft_each
      (0..@bars-1).each do |i|
        yield @fft.spectrum[i]
      end
    end

    def fft_each_with_index
      (0..@bars-1).each do |i|
        yield @fft.spectrum[i], i
      end
    end

    def stop
      Ess.stop
    end
  end
end

# Include this in a sketch to make it freeze when the mouse is down
module MouseFreeze
  def mousePressed
    self.noLoop
  end
  def mouseReleased
    self.loop
  end
end




